﻿using OxyPlot.Axes;
using OxyPlot.Series;
using OxyPlot.SkiaSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace OxyPlot.CatExtension.Skia
{
    /// <summary>
    /// 在SimdLineSeries的基础上，把double改为float，并移除了Marker的渲染支持
    /// </summary>
    public sealed class SimdLineSeriesF : XYAxisSeries
    {
        public LineLegendPosition LineLegendPosition { get; set; } = LineLegendPosition.None;
        public double StrokeThickness { get; set; } = 2;
        public LineStyle LineStyle { get; set; } = LineStyle.Automatic;
        public LineStyle ActualLineStyle
        {
            get
            {
                return LineStyle;
            }
        }
        public OxyColor Color { get; set; } = OxyColors.Automatic;
        public OxyColor ActualColor
        {
            get
            {
                if (Color == OxyColors.Automatic)
                {
                    Color = PlotModel.GetDefaultColor();
                }
                return Color;
            }
        }
        public double[]? Dashes { get; set; }
        public double[]? ActualDashArray
        {
            get
            {
                return this.Dashes ?? this.ActualLineStyle.GetDashArray();
            }
        }
        public LineJoin LineJoin { get; set; } = LineJoin.Bevel;        


        private readonly List<float> decimatorBufferX = [];
        private readonly List<float> decimatorBufferY = [];

        public List<float> PointsX { get; init; } = [];
        public List<float> PointsY { get; init; } = [];

        private readonly SimdDecimatorF decimator = new();

        public void AddPoint(float x, float y)
        {
            PointsX.Add(x);
            PointsY.Add(y);
        }

        public void AddPoints(Span<float> x, Span<float> y)
        {
            PointsX.AddRange(x);
            PointsY.AddRange(y);
        }

        public void AddPoints(IEnumerable<float> x, IEnumerable<float> y)
        {
            PointsX.AddRange(x);
            PointsY.AddRange(y);
        }        

        public void ClearPoints()
        {
            PointsX.Clear();
            PointsY.Clear();
        }

        protected override void UpdateMaxMin()
        {
            if (PointsX.Count == 0)
            {
                base.UpdateMaxMin();
                return;
            }

            MinX = PointsX[0];
            MaxX = PointsX[^1];

            (MinY, MaxY) = SIMD.MinMax(CollectionsMarshal.AsSpan(PointsY));
        }

        public override void Render(IRenderContext rc)
        {
            if (PointsX.Count == 0) return;

            this.VerifyAxes();
            this.RenderPoints(rc);

            if (this.LineLegendPosition != LineLegendPosition.None && !string.IsNullOrEmpty(this.Title))
            {
                this.RenderLegendOnLine(rc);
            }
        }

        public void RenderPoints(IRenderContext rc)
        {
            var (start, end) = GetScreenDataRange();
            decimator.Decimate(PointsX, PointsY, start, end, (float)XAxis.Offset, (float)XAxis.Scale, (float)YAxis.Offset, (float)YAxis.Scale, this.decimatorBufferX, this.decimatorBufferY);

            this.RenderLines(rc, decimatorBufferX, decimatorBufferY);
            decimatorBufferX.Clear();
            decimatorBufferY.Clear();
        }

        /// <summary>
        /// Renders a legend on the line.
        /// </summary>
        /// <param name="rc">The render context.</param>
        public void RenderLegendOnLine(IRenderContext rc)
        {
            // Find the position
            int point;
            HorizontalAlignment ha;
            var va = VerticalAlignment.Middle;
            double dx = 4;

            switch (this.LineLegendPosition)
            {
                case LineLegendPosition.Start:
                    point = 0;
                    ha = HorizontalAlignment.Right;
                    dx = -dx;
                    break;
                case LineLegendPosition.End:
                    point = PointsX.Count - 1;
                    ha = HorizontalAlignment.Left;
                    break;
                case LineLegendPosition.None:
                    return;
                default:
                    throw new ArgumentOutOfRangeException();
            }

            this.Orientate(ref ha, ref va);
            var pt = this.Transform(PointsX[point], PointsY[point]) + this.Orientate(new ScreenVector(dx, 0));

            // Render the legend
            rc.DrawText(
                pt,
                this.Title,
                this.ActualTextColor,
                this.ActualFont,
                this.ActualFontSize,
                this.ActualFontWeight,
            0,
            ha,
            va);
        }

        /// <summary>
        /// Renders the legend symbol for the line series on the
        /// specified rendering context.
        /// </summary>
        /// <param name="rc">The rendering context.</param>
        /// <param name="legendBox">The bounding rectangle of the legend box.</param>
        public override void RenderLegend(IRenderContext rc, OxyRect legendBox)
        {            
            double ymid = (legendBox.Top + legendBox.Bottom) / 2;

            rc.DrawLine(
                [new ScreenPoint(legendBox.Left, ymid), new ScreenPoint(legendBox.Right, ymid)],
                this.GetSelectableColor(this.ActualColor),
                this.StrokeThickness,
                this.EdgeRenderingMode,
                this.ActualDashArray);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public ScreenPoint Transform(double x, double y)
        {
            return new ScreenPoint(XAxis.Transform(x), YAxis.Transform(y));
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private (int, int) GetScreenDataRange()
        {
            var span = CollectionsMarshal.AsSpan(PointsX);

            var xmin = this.XAxis.ClipMinimum;
            var xmax = this.XAxis.ClipMaximum;

            var start = span.BinarySearch((float)xmin);
            if (start < 0) start = ~start - 1;
            var end = span.BinarySearch((float)xmax);
            if (end < 0) end = ~end;

            return (Math.Max(start - 1, 0), Math.Min(end + 1, PointsX.Count - 1));
        }

        /// <summary>
        /// Renders the transformed points as a line (smoothed if <see cref="InterpolationAlgorithm"/> isn’t <c>null</c>) and markers (if <see cref="MarkerType"/> is not <c>None</c>).
        /// </summary>
        /// <param name="rc">The render context.</param>
        /// <param name="pointsToRender">The points to render.</param>
        private void RenderLines(IRenderContext rc, List<float> pointsToRenderX, List<float> pointsToRenderY)
        {
            // clip the line segments with the clipping rectangle
            if (this.StrokeThickness > 0 && this.LineStyle != LineStyle.None)
            {
                this.RenderLine(rc, pointsToRenderX, pointsToRenderY);
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private void RenderLine(IRenderContext rc, List<float> pointsToRenderX, List<float> pointsToRenderY)
        {
            var dashArray = this.ActualDashArray;

            DrawReducedLine(
                rc,
                pointsToRenderX,
                pointsToRenderY,
                this.GetSelectableColor(this.ActualColor),
                this.StrokeThickness,
                this.EdgeRenderingMode,
                dashArray,
                this.LineJoin);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static void DrawReducedLine(IRenderContext rc, List<float> pointsX, List<float> pointsY, OxyColor stroke, double strokeThickness, EdgeRenderingMode edgeRenderingMode, double[]? dashArray, LineJoin lineJoin)
        {
            var spanX = CollectionsMarshal.AsSpan(pointsX);
            var spanY = CollectionsMarshal.AsSpan(pointsY);            
            ((SkiaRenderContext)rc).DrawLine(spanX, spanY, stroke, strokeThickness, edgeRenderingMode, dashArray, lineJoin);
        }


        public override TrackerHitResult? GetNearestPoint(ScreenPoint point, bool interpolate)
        {
            if (XAxis == null || YAxis == null || PointsX.Count == 0)
            {
                return null;
            }

            TrackerHitResult? result;
            if (interpolate)
            {
                result = GetNearestInterpolatedPoint(point);
            }
            else
            {
                result = GetNearestPoint(point);
            }

            if (result != null)
            {
                result.Text = StringHelper.Format(
                    this.ActualCulture,
                    this.TrackerFormatString,
                    result.Item,
                    this.Title,
                    this.XAxis.Title ?? XYAxisSeries.DefaultXAxisTitle,
                    this.XAxis.GetValue(result.DataPoint.X),
                    this.YAxis.Title ?? XYAxisSeries.DefaultYAxisTitle,
                    this.YAxis.GetValue(result.DataPoint.Y));
            }

            return result;

        }

        private TrackerHitResult? GetNearestInterpolatedPoint(ScreenPoint screenPoint)
        {
            var dataPoint = InverseTransform(screenPoint);

            var xSpan = CollectionsMarshal.AsSpan(PointsX);
            var ySpan = CollectionsMarshal.AsSpan(PointsY);

            var p1Index = xSpan.BinarySearch((float)dataPoint.X);
            if (p1Index < 0) p1Index = Math.Max(~p1Index - 1, 0);


            int p2Index;

            if (p1Index == 0 || p1Index == xSpan.Length - 1)
            {
                p2Index = p1Index;
            }
            else if (xSpan[p1Index] < dataPoint.X)
            {
                p2Index = p1Index + 1;
            }
            else
            {
                p2Index = p1Index - 1;
            }

            double dx, dy;

            if (p1Index == p2Index)
            {
                dx = xSpan[p1Index];
                dy = ySpan[p1Index];
            }
            else
            {
                var k1 = (ySpan[p2Index] - ySpan[p1Index]) / (xSpan[p2Index] - xSpan[p1Index]);
                var b1 = ySpan[p1Index] - k1 * xSpan[p1Index];

                dx = dataPoint.X;
                dy = k1 * dx + b1;
            }

            dataPoint = new DataPoint(dx, dy);
            screenPoint = Transform(dataPoint);

            var index = Math.Min(p1Index, p2Index);

            return new TrackerHitResult
            {
                Series = this,
                DataPoint = dataPoint,
                Position = screenPoint,
                Index = index,
                Item = new DataPoint(xSpan[index], ySpan[index])
            };
        }

        private TrackerHitResult? GetNearestPoint(ScreenPoint screenPoint)
        {
            var dataPoint = InverseTransform(screenPoint);

            var xSpan = CollectionsMarshal.AsSpan(PointsX);

            var p1Index = xSpan.BinarySearch((float)dataPoint.X);
            if (p1Index < 0) p1Index = Math.Max(~p1Index - 1, 0);

            int p2Index;
            if (p1Index == 0 || p1Index == xSpan.Length - 1)
            {
                p2Index = p1Index;
            }
            else if (xSpan[p1Index] < dataPoint.X)
            {
                p2Index = p1Index + 1;
            }
            else
            {
                p2Index = p1Index - 1;
            }

            var index = Math.Min(p1Index, p2Index);

            dataPoint = new DataPoint(xSpan[index], PointsY[index]);
            screenPoint = Transform(dataPoint);

            return new TrackerHitResult
            {
                Series = this,
                DataPoint = dataPoint,
                Position = screenPoint,
                Index = index,
                Item = dataPoint
            };
        }
    }
}
